CloudFormation StackSets のデプロイ失敗をSlack通知する

CloudFormation StackSets のデプロイ失敗をSlack通知する

Clock Icon2024.07.14

こんにちは。たかやまです。

今回は、こちらの記事のSlack通知版を作成したいと思います。

https://dev.classmethod.jp/articles/notify-cfn-stacksets-deploy-fail/

メール通知をしたい場合には上記記事を参考にしてください。

とりあえずやってみたい方へ

CFnテンプレート

Chatbot版

前提条件 :

Slackワークスペースは設定済みで、SlackワークスペースIDとチャンネルIDを取得済みであること

パラメータ:

  • SlackWorkspaceId : SlackワークスペースID
  • SlackChannelId : SlackチャンネルID
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Slack Configuration
        Parameters:
          - SlackWorkspaceId
          - SlackChannelId
    ParameterLabels:
      SlackWorkspaceId:
        default: Slack Workspace ID
      SlackChannelId:
        default: Slack Channel ID
Parameters:
  SlackWorkspaceId:
    Type: String
    Description: Slack Workspace ID
  SlackChannelId:
    Type: String
    Description: Slack Channel ID
Resources:
  ChatbotErrorTopicF26444E6:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: ChatbotErrorNotifications
  ChatbotErrorTopicPolicy0A68C558:
    Type: AWS::SNS::TopicPolicy
    Properties:
      PolicyDocument:
        Statement:
          - Action: sns:Publish
            Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Resource:
              Ref: ChatbotErrorTopicF26444E6
            Sid: "0"
        Version: "2012-10-17"
      Topics:
        - Ref: ChatbotErrorTopicF26444E6
  ChatbotErrorSlackChannelConfigurationRole0D1A29D8:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: chatbot.amazonaws.com
        Version: "2012-10-17"
  ChatbotErrorSlackChannel461DCF21:
    Type: AWS::Chatbot::SlackChannelConfiguration
    Properties:
      ConfigurationName: chatbot-error-notifications
      IamRoleArn:
        Fn::GetAtt:
          - ChatbotErrorSlackChannelConfigurationRole0D1A29D8
          - Arn
      LoggingLevel: ERROR
      SlackChannelId:
        Ref: SlackChannelId
      SlackWorkspaceId:
        Ref: SlackWorkspaceId
      SnsTopicArns:
        - Ref: ChatbotErrorTopicF26444E6
  ChatbotErrorRuleF0295058:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.cloudformation
        detail-type:
          - CloudFormation StackSet StackInstance Status Change
        detail:
          status-details:
            detailed-status:
              - INOPERABLE
              - CANCELLED
              - FAILED
              - FAILED_IMPORT
              - SKIPPED_SUSPENDED_ACCOUNT
      State: ENABLED
      Targets:
        - Arn:
            Ref: ChatbotErrorTopicF26444E6
          Id: Target0
          InputTransformer:
            InputPathsMap:
              region: $.region
              account: $.account
              detail-stack-set-arn: $.detail.stack-set-arn
              detail-status-details-status: $.detail.status-details.status
              detail-status-details-detailed-status: $.detail.status-details.detailed-status
              detail-status-details-status-reason: $.detail.status-details.status-reason
            InputTemplate: '{"version":"1.0","source":"custom","content":{"textType":"client-markdown","title":":warning: CloudFormation StackSet StackInstance Status Change | <region> | Account: <account>","description":"アカウントID: <account>\nリージョン: <region>\n\nスタックセットARN: <detail-stack-set-arn>\n\nステータス: <detail-status-details-status>\nステータス詳細: <detail-status-details-detailed-status>\nステータス理由: <detail-status-details-status-reason>"}}':
Slack Webhook版

前提条件 :

Slack Webhook URLを取得済みであること

パラメータ:

  • SlackWebhookUrl : Slack Webhook URL
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Slack Configuration
        Parameters:
          - SlackWebhookUrl
    ParameterLabels:
      SlackWebhookUrl:
        default: Slack Webhook URL
Parameters:
  SlackWebhookUrl:
    Type: String
    Description: Slack Webhook URL
Resources:
  SlackNotifierLambdaServiceRoleBF51634B:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  SlackNotifierLambdaServiceRoleDefaultPolicy9623029E:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action: organizations:DescribeAccount
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
      PolicyName: SlackNotifierLambdaServiceRoleDefaultPolicy9623029E
      Roles:
        - Ref: SlackNotifierLambdaServiceRoleBF51634B
  SlackNotifierLambda0EBEA281:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |-

          import boto3
          import os
          import json
          import urllib.request
          from logging import getLogger, INFO

          logger = getLogger()
          logger.setLevel(INFO)

          organizations = boto3.client("organizations")

          SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']

          def get_account_name(account_id):
              resp = organizations.describe_account(AccountId=account_id)
              return resp['Account']['Name']

          def lambda_handler(event, context):
              # メッセージ作成に必要なパラメータの取得
              detail_type = event['detail-type']
              stack_set_arn = event['detail'].get('stack-set-arn')
              stack_set_name = stack_set_arn.split('/')[1].split(':')[0]
              stack_id = event['detail'].get('stack-id')
              region = stack_id.split(':')[3]
              account_id = stack_id.split(':')[4]
              account_name = get_account_name(account_id)
              status = event['detail']['status-details'].get('status')
              detailed_status = event['detail']['status-details'].get('detailed-status')
              status_reason = event['detail']['status-details'].get('status-reason')

              # メッセージ作成
              title = f":warning: *{detail_type} | {region} | Account: {account_id}*"
              message = (f"{title}\n"
                         f"アカウント: {account_name} ({account_id})\n"
                         f"リージョン: {region}\n\n"
                         f"スタックセット名: {stack_set_name}\n"
                         f"スタックセットARN: {stack_set_arn}\n\n"
                         f"ステータス: {status}\n"
                         f"ステータス詳細: {detailed_status}\n"
                         f"ステータス理由: {status_reason}")

              # Slack通知
              slack_message = {
                  'text': message
              }

              data = json.dumps(slack_message).encode('utf-8')
              req = urllib.request.Request(SLACK_WEBHOOK_URL, data=data, headers={'Content-Type': 'application/json'})
              try:
                  response = urllib.request.urlopen(req)
                  response_body = response.read()
                  logger.info(f'Slack notification sent successfully: {response_body}')
              except urllib.error.HTTPError as e:
                  logger.error(f'Failed to send Slack notification: {e.reason}')
                  raise

              return {
                  'statusCode': 200,
                  'body': json.dumps('Slack notification sent successfully')
              }  # この行の括弧を追加しました

      Environment:
        Variables:
          SLACK_WEBHOOK_URL:
            Ref: SlackWebhookUrl
      Handler: index.lambda_handler
      Role:
        Fn::GetAtt:
          - SlackNotifierLambdaServiceRoleBF51634B
          - Arn
      Runtime: python3.12
    DependsOn:
      - SlackNotifierLambdaServiceRoleDefaultPolicy9623029E
      - SlackNotifierLambdaServiceRoleBF51634B
  StackSetsErrorRuleFBA6F6EB:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.cloudformation
        detail-type:
          - CloudFormation StackSet StackInstance Status Change
        detail:
          status-details:
            detailed-status:
              - INOPERABLE
              - CANCELLED
              - FAILED
              - FAILED_IMPORT
              - SKIPPED_SUSPENDED_ACCOUNT
      State: ENABLED
      Targets:
        - Arn:
            Fn::GetAtt:
              - SlackNotifierLambda0EBEA281
              - Arn
          Id: Target0
  StackSetsErrorRuleAllowEventRuleWebhookNotifyStackSlackNotifierLambdaD780FA364FFD0DEE:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Fn::GetAtt:
          - SlackNotifierLambda0EBEA281
          - Arn
      Principal: events.amazonaws.com
      SourceArn:
        Fn::GetAtt:
          - StackSetsErrorRuleFBA6F6EB
          - Arn

CDKテンプレート

CDKのコードも用意しているので、CDKを利用される方はこちらからご利用ください。

https://github.com/nyankotaro/cm-stacksets-notify/tree/main

Chatbot版(シンプル版)

こちらはAWS Chatbotを利用してSlackに通知を送る方法です。

以下のようにCANCELLEDFAILEDのステータスエラーをStackSetsで発生させ通知させてみたいと思います。

01-cloudformation-stacksets-fail-slack

冒頭のChatbot版テンプレートをデプロイしてSlackに通知された内容がこちらです。

02-cloudformation-stacksets-fail-slack

Chatbotのデフォルト通知はエラー内容がわからないため、こちらの通知はEventBridgeの入力トランスフォーマーを利用して通知内容をカスタマイズしています。

Chatbotデフォルト通知

03-cloudformation-stacksets-fail-slack

ただ、入力トランスフォーマーもイベント元の内容を元に整形しているため、アカウント名などの元情報に存在しない情報は付与することはできません。

通知内容にAWSアカウント名を付与したいなど内容をよりカスタマイズしたい場合には、次にご紹介するLambda関数を利用してSlack Webhookを利用して通知します。

Slack Webhook版(AWSアカウント名を付与したい)

メール通知版と同様にアカウント名を付与する場合は、Lambda関数を利用してSlack Webhookを利用して通知します。

Lambda関数内のコードは以下のようになります。

import boto3
import os
import json
import urllib.request
from logging import getLogger, INFO

logger = getLogger()
logger.setLevel(INFO)

organizations = boto3.client("organizations")

SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']

def get_account_name(account_id):
    resp = organizations.describe_account(AccountId=account_id)
    return resp['Account']['Name']

def lambda_handler(event, context):
    # #####
    # メッセージ作成に必要なパラメータの取得
    # #####
    detail_type = event['detail-type']
    # ### スタックセット名
    stack_set_arn = event['detail'].get('stack-set-arn')
    stack_set_name = stack_set_arn.split('/')[1].split(':')[0]
    # ### リージョン, アカウントID, アカウント名
    stack_id = event['detail'].get('stack-id')
    region = stack_id.split(':')[3]
    account_id = stack_id.split(':')[4]
    account_name = get_account_name(account_id)
    # ### ステータス, ステータス詳細, ステータス理由
    status = event['detail']['status-details'].get('status')
    detailed_status = event['detail']['status-details'].get('detailed-status')
    status_reason = event['detail']['status-details'].get('status-reason')

    # #####
    # メッセージ作成
    # #####
    title = f":warning: *{detail_type} | {region} | Account: {account_id}*"
    message = ( f'{title}\n'
                f'アカウント: {account_name} ({account_id})\n'
                f'リージョン: {region}\n\n'
                f'スタックセット名: {stack_set_name}\n'
                f'スタックセットARN: {stack_set_arn}\n\n'
                f'ステータス: {status}\n'
                f'ステータス詳細: {detailed_status}\n'
                f'ステータス理由: {status_reason}')

    # #####
    # Slack通知
    # #####
    slack_message = {
        'text': message
    }

    data = json.dumps(slack_message).encode('utf-8')
    req = urllib.request.Request(SLACK_WEBHOOK_URL, data=data, headers={'Content-Type': 'application/json'})
    try:
        response = urllib.request.urlopen(req)
        response_body = response.read()
        logger.info(f'Slack notification sent successfully: {response_body}')
    except urllib.error.HTTPError as e:
        logger.error(f'Failed to send Slack notification: {e.reason}')
        raise

    return {
        'statusCode': 200,
        'body': json.dumps('Slack notification sent successfully')
    }

Webhookを利用した場合の通知は以下のようになります。

04-cloudformation-stacksets-fail-slack

アカウント名が付与され、アカウントIDのみの通知よりもわかりやすくなりました。

最後に

CloudFormation StackSetsのデプロイ失敗をSlack通知する場合の方法をご紹介しました。

こちらの内容がどなたかのお役に立てれば幸いです。

以上、たかやま(@nyan_kotaroo)でした。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.